#Region " WikiSite Class "

''' <summary>
''' Represents a Wikia site and provides methods for querying and updating article and image content.
''' </summary>
''' <remarks>
''' An option WikiCache object can be attached to this class to allow local retrieval of
''' specific revisions of pages. If added, this can greatly improve performance since cache hits
''' will avoid the throttling time applied to live server requests.
''' </remarks>
Public Class WikiSite

#Region " Fields "

    ''' <summary>
    ''' Holds a reference to a cache of pages retrieved from the Wikia server. Only specific 
    ''' revisions of pages are held in the cache here so requests for pages by title still have
    ''' to go to the server, but any requests for specific revisions can be checked here first
    ''' to see if the cache has a local copy of the file.
    ''' </summary>
    ''' <remarks></remarks>
    Private m_Cache As WikiCache

    ''' <summary>
    ''' The root url for the Wikia site. E.g. "http://runescape.wikia.com"
    ''' </summary>
    ''' <remarks></remarks>
    Private m_SiteUrl As String

    ''' <summary>
    ''' The relative path from the SiteUrl to the virtual folder containing the index.php file.
    ''' </summary>
    ''' <remarks>
    ''' To determine the Indexpath simply request a history page on the Wikia server. The Indexpath is the
    ''' relative path between the SiteUrl and the "index.php". For
    ''' "http://runescape.wikia.com/wiki/index.php?title=Main_Page&amp;action=history" this would be "wiki".
    ''' For "http://runescape.wikia.com/index.php?title=Main_Page&amp;action=history" it is an empty string.
    ''' </remarks>
    Private m_IndexPath As String

    ''' <summary>
    ''' The timestamp for the last call to ExecuteRequest. Requests to the Wikia server are
    ''' throttled to avoid overloading the server. This field allows the ExecuteRequest to
    ''' determine how long to wait before executing the next request.
    ''' </summary>
    ''' <remarks></remarks>
    Private m_LastQueryTimestamp As Date

#End Region

#Region " Constructors "

    ''' <summary>
    ''' Creates a new WikiSite with the specified SiteUrl and an empty IndexPath.
    ''' </summary>
    ''' <param name="siteUrl">
    ''' The root url for the Wikia site. E.g. "http://runescape.wikia.com"
    ''' </param>
    ''' <remarks></remarks>
    Public Sub New(ByVal siteUrl As String)
        Me.New(siteUrl, Nothing)
    End Sub

    ''' <summary>
    ''' Creates a new WikiSite with the specified SiteUrl and IndexPath.
    ''' </summary>
    ''' <param name="siteUrl">
    ''' The root url for the Wikia site. E.g. "http://runescape.wikia.com"
    ''' </param>
    ''' <param name="indexPath">
    ''' The relative path from the SiteUrl to the virtual folder containing the index.php file.
    ''' </param>
    ''' <remarks>
    ''' To determine the Indexpath simply request a history page on the Wikia server. The Indexpath is the
    ''' relative path between the SiteUrl and the "index.php". For
    ''' "http://runescape.wikia.com/wiki/index.php?title=Main_Page&amp;action=history" this would be "wiki".
    ''' For "http://runescape.wikia.com/index.php?title=Main_Page&amp;action=history" it is an empty string.
    ''' </remarks>
    Public Sub New(ByVal siteUrl As String, ByVal indexPath As String)
        MyBase.New()
        ' validate the parameters
        If (siteUrl = String.Empty) Then Throw New System.ArgumentNullException("siteUrl")
        ' copy the parameters locally
        m_SiteUrl = siteUrl
        m_IndexPath = indexPath
    End Sub

#End Region

#Region " Properties "

    ''' <summary>
    ''' Gets or sets root url for the Wikia site. E.g. "http://runescape.wikia.com"
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property SiteUrl() As String
        Get
            Return m_SiteUrl
        End Get
    End Property

    ''' <summary>
    ''' <param name="indexPath">
    ''' Gets os sets the relative path from the SiteUrl to the virtual folder containing the index.php file.
    ''' </param>
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' To determine the Indexpath simply request a history page on the Wikia server. The Indexpath is the
    ''' relative path between the SiteUrl and the "index.php". For
    ''' "http://runescape.wikia.com/wiki/index.php?title=Main_Page&amp;action=history" this would be "wiki".
    ''' For "http://runescape.wikia.com/index.php?title=Main_Page&amp;action=history" it is an empty string.
    ''' </remarks>
    Public ReadOnly Property IndexPath() As String
        Get
            Return m_IndexPath
        End Get
    End Property

    ''' <summary>
    ''' Gets or sets a reference to a cache of pages retrieved from the Wikia server. 
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks>
    ''' Only specific revisions of pages are held in the cache so requests for pages by title
    ''' still have to go to the server, but any requests for specific revisions can be checked
    ''' here first to see if the cache has a local copy of the file.
    ''' </remarks>
    Public Property Cache() As WikiCache
        Get
            Return m_Cache
        End Get
        Set(ByVal Value As WikiCache)
            m_Cache = Value
        End Set
    End Property

#End Region

#Region " Methods "

#Region " HTTP Methods "

    ''' <summary>
    ''' Creates a System.Net.HttpWebRequest using some default property values appropriate for
    ''' interacting with MediaWiki sites.
    ''' </summary>
    ''' <param name="cookies">The cookies to pass into the request.</param>
    ''' <param name="url">The target url to create the request for.</param>
    ''' <returns>Returns a new initialised System.Net.HttpWebRequest</returns>
    ''' <remarks></remarks>
    Friend Function CreateRequest(ByVal cookies As System.Net.CookieContainer, ByVal url As String) As System.Net.HttpWebRequest
        Dim objRequest As System.Net.HttpWebRequest
        ' create the new request object
        objRequest = CType(System.Net.HttpWebRequest.Create(New Uri(url)), System.Net.HttpWebRequest)
        ' set some default properties
        objRequest.AllowAutoRedirect = False
        objRequest.Headers.Add("Cache-Control", "no-cache")
        objRequest.Headers.Add("Pragma", "no-cache")
        objRequest.CookieContainer = cookies
        ' return the result
        Return objRequest
    End Function

    ''' <summary>
    ''' Provides a mechanism to throttle web requests to the MediaWiki server.
    ''' </summary>
    ''' <param name="request">
    ''' The request object to execute once a specific timeout has expired.
    ''' The function ensures that a minimum time has elapsed between requests before it will
    ''' action the next one.
    ''' </param>
    ''' <returns>Returns the System.Net.HttpWebResponse generated by executing the HttpRequest.</returns>
    ''' <remarks>
    ''' HttpRequests are throttled to avoid overloading the Wikia server. This method blocks until the
    ''' timeout from the previous request has expired.
    ''' </remarks>
    Friend Function ExecuteRequest(ByVal request As System.Net.HttpWebRequest) As System.Net.HttpWebResponse
        Return Me.ExecuteRequest(request, Nothing)
    End Function

    ''' <summary>
    ''' Provides a mechanism to throttle web requests to the MediaWiki server.
    ''' </summary>
    ''' <param name="request">The request object to execute once a specific timeout has expired.
    ''' The function ensures that a minimum time has elapsed between requests before it will
    ''' action the next one.</param>
    ''' <param name="postData">Binary data to POST in the body of the request.</param>
    ''' <returns>Returns the System.Net.HttpWebResponse generated by executing the HttpRequest.</returns>
    ''' <remarks>
    ''' HttpRequests are throttled to avoid overloading the Wikia server. This method blocks until the
    ''' timeout from the previous request has expired.
    ''' </remarks>
    Friend Function ExecuteRequest(ByVal request As System.Net.HttpWebRequest, ByVal postData() As Byte) As System.Net.HttpWebResponse
        Dim c_IntervalMilliSeconds As Integer = 10000
        Dim objResponse As System.Net.HttpWebResponse
        Dim objRequestStream As System.IO.Stream
        Dim intSleepTime As Integer
        Debug.WriteLine("executing " & request.RequestUri.ToString)
        ' throttle page requests to avoid overloading the server
        If (Now < m_LastQueryTimestamp.AddMilliseconds(c_IntervalMilliSeconds)) Then
            intSleepTime = CType(m_LastQueryTimestamp.AddMilliseconds(c_IntervalMilliSeconds).Subtract(Now).TotalMilliseconds, Integer)
            Debug.WriteLine("    waiting " & intSleepTime & " milliseconds to send request")
            System.Threading.Thread.Sleep(intSleepTime)
        End If
        ' post the data
        If Not (postData Is Nothing) Then
            request.ContentLength = postData.Length
            objRequestStream = request.GetRequestStream()
            objRequestStream.Write(postData, 0, postData.Length)
            objRequestStream.Close()
        End If
        ' execute the request
        objResponse = CType(request.GetResponse(), System.Net.HttpWebResponse)
        m_LastQueryTimestamp = Now
        ' return the result
        Return objResponse
    End Function

    ''' <summary>
    ''' Performs a simple HTTP GET on the specified url.
    ''' </summary>
    ''' <param name="cookies">Cookies to send with the request.</param>
    ''' <param name="url">The url to query.</param>
    ''' <returns>The body text of the response from the specified url.</returns>
    ''' <remarks>
    ''' HttpRequests are throttled to avoid overloading the Wikia server. This method blocks until the
    ''' timeout from the previous request has expired.
    ''' </remarks>
    Private Function ExecuteUrl(ByVal cookies As System.Net.CookieContainer, ByVal url As String) As String
        Dim objRequest As System.Net.HttpWebRequest
        Dim objResponse As System.Net.HttpWebResponse
        Dim strResponseBody As String
        ' query the wiki server
        objRequest = Me.CreateRequest(cookies, url)
        objResponse = Me.ExecuteRequest(objRequest)
        If Not (objResponse.StatusCode = Net.HttpStatusCode.OK) Then
            Throw New System.InvalidOperationException
        End If
        strResponseBody = Me.GetResponseStreamText(objResponse)
        objResponse.Close()
        ' return the result
        Return strResponseBody
    End Function

    ''' <summary>
    ''' Reads the body of a HTTP response and returns the content as a string. 
    ''' </summary>
    ''' <param name="response"></param>
    ''' <returns>A string containing the body text of the specified HttpWebResponse</returns>
    ''' <remarks></remarks>
    Private Function GetResponseStreamText(ByVal response As System.Net.HttpWebResponse) As String
        Dim objResponseStream As System.IO.Stream
        Dim objResponseReader As System.IO.StreamReader
        Dim strResponseBody As String
        ' read the response stream as a string
        objResponseStream = response.GetResponseStream
        objResponseReader = New System.IO.StreamReader(objResponseStream)
        strResponseBody = objResponseReader.ReadToEnd
        objResponseReader.Close()
        ' return the result
        Return strResponseBody
    End Function

#End Region

#Region " Page Methods "

    ''' <summary>
    ''' Gets a collection of WikiPage objects that represent the revision history of the specified article.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <parameter name="title">The title of the article to get the history for. This should be
    ''' non-urlencoded as it will be UrlEncoded inside the member.</parameter>
    ''' <param name="limit">The maximum number of history items to return. If the article's
    ''' revision history is longer than this value only the most recent revisions are included
    ''' in the returned collection.</param>
    ''' <returns>
    ''' A WikiPageCollection containing the WikiPage revisions that represent the history
    ''' for the specified article.
    ''' </returns>
    ''' <remarks>
    ''' Only the EditDate, EditUser, EditMinor, EditComment and RevisionId properties are
    ''' populated in the returned collection. To populate the page body of all revisions in the 
    ''' collection use the WikiSite.GetPageHistory overload with getBody = true. To retrieve the
    ''' body text for single revision use WikiSite.GetPageBody.
    ''' <br/>
    ''' The returned collection will be collated in reverse chronological order, with the most recent
    ''' revision in the first index. Use WikiPageCollection.Reverse to sort the collection in
    ''' chronological order.
    ''' </remarks>
    Friend Function GetPageHistory(ByVal cookies As System.Net.CookieContainer, ByVal title As String, ByVal limit As Integer) As WikiPageCollection
        ' return the result
        Return Me.GetPageHistory(cookies, title, limit, False)
    End Function

    ''' <summary>
    ''' Gets a collection of WikiPage objects that represent the revision history of the specified article.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <param name="title">The title of the article to get the history for. This should be
    ''' non-urlencoded as it will be UrlEncoded inside the member.</param>
    ''' <param name="limit">The maximum number of history items to return. If the article's
    ''' revision history is longer than this value only the most recent revisions are included
    ''' in the returned collection.</param>
    ''' <param name="getBody">If true, specifies that the Body property of each revision should
    ''' be populated. This may </param>
    ''' <returns>
    ''' </returns>
    ''' <remarks>
    ''' Retrieves the page history and then subsequently makes calls to retrieve the body text
    ''' for each revision if getBody is true.
    ''' 
    ''' The returned collection will be collated in reverse chronological order, with the most recent
    ''' revision in the first index. Use WikiPageCollection.Reverse to sort the collection in
    ''' chronological order.
    ''' </remarks>
    Friend Function GetPageHistory(ByVal cookies As System.Net.CookieContainer, ByVal title As String, ByVal limit As Integer, ByVal getBody As Boolean) As WikiBotLib.WikiPageCollection
        Dim objRequestUri As System.Text.StringBuilder
        Dim strResponseBody As String
        Dim objResponseXml As System.Xml.XmlDocument
        Dim objResponseNodes As System.Xml.XmlNodeList
        Dim objWikiPages As WikiPageCollection
        Dim objWikiPage As WikiPage
        Dim strTempValue As String
        ' build the uri to query
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        If Not (Me.IndexPath = String.Empty) Then
            objRequestUri.Append("/" & Me.IndexPath)
        End If
        objRequestUri.Append("/index.php?")
        objRequestUri.Append("title=" & System.Web.HttpUtility.UrlEncode(title))
        objRequestUri.Append("&action=history")
        objRequestUri.Append("&limit=" & limit)
        objRequestUri.Append("&ctype=text/plain")
        objRequestUri.Append("&dontcountme=s")
        ' query the wiki server
        strResponseBody = Me.ExecuteUrl(cookies, objRequestUri.ToString)
        ' extract the history entries
        strResponseBody = strResponseBody.Substring(strResponseBody.IndexOf("<ul id=""pagehistory"">"))
        strResponseBody = strResponseBody.Substring(0, strResponseBody.IndexOf("</ul>") + 5)
        ' read the wiki response into an xml document
        objResponseXml = New System.Xml.XmlDocument
        objResponseXml.LoadXml(strResponseBody)
        ' convert revision nodes into WikiPage objects
        objResponseNodes = objResponseXml.DocumentElement.SelectNodes("li")
        objWikiPages = New WikiPageCollection
        For Each objResponseNode As System.Xml.XmlNode In objResponseNodes
            objWikiPage = New WikiPage(Me, title)
            objWikiPage.EditDate = Date.ParseExact(objResponseNode.SelectSingleNode(".//span[@class='history-user']").PreviousSibling.InnerText, "HH:mm, d MMMM yyyy", System.Globalization.CultureInfo.InvariantCulture)
            objWikiPage.EditUser = objResponseNode.SelectSingleNode(".//span[@class='history-user']/a").InnerText
            objWikiPage.EditMinor = Not (objResponseNode.SelectSingleNode(".//span[@class='minor']") Is Nothing)
            If Not (objResponseNode.SelectSingleNode(".//span[@class='comment']") Is Nothing) Then
                objWikiPage.EditComment = objResponseNode.SelectSingleNode(".//span[@class='comment']").InnerText
            End If
            ' get the oldid from the first link url
            strTempValue = objResponseNode.SelectSingleNode(".//span[@class='history-user']").PreviousSibling.Attributes.GetNamedItem("href").Value
            strTempValue = strTempValue.Substring(strTempValue.IndexOf("oldid="))
            strTempValue = strTempValue.Substring("oldid=".Length)
            objWikiPage.RevisionId = CType(strTempValue, Integer)
            objWikiPages.Add(objWikiPage)
        Next objResponseNode
        ' populate the body property if required
        If getBody Then
            ' check each revision to see if it's in the cache, and download it if not
            For intIndex As Integer = (objWikiPages.Count - 1) To 0 Step -1
                If (Not Me.Cache Is Nothing) AndAlso (Me.Cache.ContainsPage(objWikiPages(intIndex).RevisionId)) Then
                    objWikiPages(intIndex) = Me.Cache.RetrieveCachePage(objWikiPages(intIndex).RevisionId)
                    objWikiPages(intIndex).Site = Me
                Else
                    objWikiPages(intIndex).Body = Me.GetPageBody(cookies, objWikiPages(intIndex).Title, objWikiPages(intIndex).RevisionId)
                    If Not (Me.Cache Is Nothing) Then
                        Me.Cache.StoreCachePage(objWikiPages(intIndex))
                    End If
                End If
            Next intIndex
        End If
        ' return the page collection
        Return objWikiPages
    End Function

    ''' <summary>
    ''' Gets the body text of the most recent version of the specified article.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <parameter name="title">The title of the article to get the history for. This should be
    ''' non-urlencoded as it will be UrlEncoded inside the member.</parameter>
    ''' <returns>
    ''' Returns the body text of the specified article.
    ''' </returns>
    ''' <remarks></remarks>
    Friend Function GetPageBody(ByVal cookies As System.Net.CookieContainer, ByVal title As String) As String
        Return Me.GetPageBody(cookies, title, -1)
    End Function

    ''' <summary>
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <parameter name="title">The title of the article to get the history for. This should be
    ''' non-urlencoded as it will be UrlEncoded inside the member.</parameter>
    ''' <param name="revisionId">The id of the revision of the article to retrieve. Pass -1
    ''' to retrieve the most recent version of the specified article regardless of its revision id.</param>
    ''' <returns>
    ''' Returns the body text of the specified article.
    ''' </returns>
    ''' <remarks></remarks>
    Friend Function GetPageBody(ByVal cookies As System.Net.CookieContainer, ByVal title As String, ByVal revisionId As Integer) As String
        Dim objRequestUri As System.Text.StringBuilder
        Dim strResponseBody As String
        ' check if the page is stored in the cache before we query the wiki
        If (Not Me.Cache Is Nothing) AndAlso Me.Cache.ContainsPage(revisionId) Then
            strResponseBody = Me.Cache.RetrieveCachePage(revisionId).Body
        Else
            ' build the uri to query
            objRequestUri = New System.Text.StringBuilder
            objRequestUri.Append(Me.SiteUrl)
            If Not (Me.IndexPath = String.Empty) Then
                objRequestUri.Append("/" & Me.IndexPath)
            End If
            objRequestUri.Append("/index.php?")
            objRequestUri.Append("title=" & System.Web.HttpUtility.UrlEncode(title))
            objRequestUri.Append("&action=raw")
            If Not (revisionId = -1) Then
                objRequestUri.Append("&oldid=" & revisionId)
            End If
            objRequestUri.Append("&ctype=text/plain")
            objRequestUri.Append("&dontcountme=s")
            ' query the wiki server
            strResponseBody = Me.ExecuteUrl(cookies, objRequestUri.ToString)
        End If
        ' return the result
        Return strResponseBody
    End Function

    ''' <summary>
    ''' Updates an article with the specified contents.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <param name="page">The page to update. The title property identifies the article to update, 
    ''' and the body property identifies the text to update the page to</param>
    ''' <param name="summary">The summary text shown by the Wikia server for this edit.</param>
    ''' <param name="minor">Indicates whether this is a minor update or not.</param>
    ''' <param name="watchThis">Specifies whether the page should be marked as a watched page.</param>
    ''' <remarks></remarks>
    Friend Sub SavePage(ByVal cookies As System.Net.CookieContainer, ByVal page As WikiPage, ByVal summary As String, ByVal minor As Boolean, ByVal watchThis As Boolean)
        Me.SavePage(cookies, page.Title, page.Body, summary, minor, watchThis)
    End Sub

    ''' <summary>
    ''' Updates an article with the specified contents.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <parameter name="title">The title of the article to get the history for. This should be
    ''' non-urlencoded as it will be UrlEncoded inside the member.</parameter>
    ''' <param name="body">The body text to update the article to.</param>
    ''' <param name="summary">The summary text shown by the Wikia server for this edit.</param>
    ''' <param name="minor">Indicates whether this is a minor update or not.</param>
    ''' <param name="watchThis">Specifies whether the page should be marked as a watched page.</param>
    ''' <remarks></remarks>
    Friend Sub SavePage(ByVal cookies As System.Net.CookieContainer, ByVal title As String, ByVal body As String, ByVal summary As String, ByVal minor As Boolean, ByVal watchThis As Boolean)
        Dim strResponseBody As String
        Dim strWpEditToken As String
        Dim strWpAutoSummary As String
        Dim strWpStartTime As String
        Dim strWpEditTime As String
        Dim strBoundary As String
        Dim objRequestUri As System.Text.StringBuilder
        Dim objRequest As System.Net.HttpWebRequest
        Dim objPostData As System.Text.StringBuilder
        Dim objPostDataBytes() As Byte
        Dim objResponse As System.Net.HttpWebResponse
        ' we have to GET the page in edit mode first so we can extract an edit token that we need to submit with the update
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        If Not (Me.IndexPath = String.Empty) Then
            objRequestUri.Append("/" & Me.IndexPath)
        End If
        objRequestUri.Append("/index.php?")
        objRequestUri.Append("title=" & System.Web.HttpUtility.UrlEncode(title))
        objRequestUri.Append("&action=edit")
        strResponseBody = Me.ExecuteUrl(cookies, objRequestUri.ToString)
        ' extract the edit token
        strWpEditToken = strResponseBody
        strWpEditToken = strWpEditToken.Substring(strWpEditToken.IndexOf("<div class='templatesUsed'>"))
        strWpEditToken = strWpEditToken.Substring(strWpEditToken.IndexOf("<input"))
        strWpEditToken = strWpEditToken.Substring(0, strWpEditToken.IndexOf("/>") + "/>".Length)
        If (strWpEditToken.IndexOf("name=""wpEditToken""") = -1) Then
            Throw New System.InvalidOperationException("failed to get edit token")
        End If
        strWpEditToken = strWpEditToken.Substring(strWpEditToken.IndexOf("value=""") + "value=""".Length)
        strWpEditToken = strWpEditToken.Substring(0, strWpEditToken.IndexOf(""""))
        ' extract the autosummary
        strWpAutoSummary = strResponseBody
        strWpAutoSummary = strWpAutoSummary.Substring(strWpAutoSummary.IndexOf("<div class='templatesUsed'>"))
        strWpAutoSummary = strWpAutoSummary.Substring(strWpAutoSummary.IndexOf("<input") + "<input".Length)
        strWpAutoSummary = strWpAutoSummary.Substring(strWpAutoSummary.IndexOf("<input"))
        strWpAutoSummary = strWpAutoSummary.Substring(0, strWpAutoSummary.IndexOf("/>") + "/>".Length)
        If (strWpAutoSummary.IndexOf("name=""wpAutoSummary""") = -1) Then
            Throw New System.InvalidOperationException("failed to get autosummary")
        End If
        strWpAutoSummary = strWpAutoSummary.Substring(strWpAutoSummary.IndexOf("value=""") + "value=""".Length)
        strWpAutoSummary = strWpAutoSummary.Substring(0, strWpAutoSummary.IndexOf(""""))
        ' extract the start time
        strWpStartTime = strResponseBody
        strWpStartTime = strWpStartTime.Substring(strWpStartTime.IndexOf("<form id=""editform"" "))
        strWpStartTime = strWpStartTime.Substring(strWpStartTime.IndexOf("<input") + "<input".Length)
        strWpStartTime = strWpStartTime.Substring(strWpStartTime.IndexOf("<input") + "<input".Length)
        strWpStartTime = strWpStartTime.Substring(strWpStartTime.IndexOf("<input"))
        strWpStartTime = strWpStartTime.Substring(0, strWpStartTime.IndexOf("/>") + "/>".Length)
        If (strWpStartTime.IndexOf("name=""wpStarttime""") = -1) Then
            Throw New System.InvalidOperationException("failed to get start time")
        End If
        strWpStartTime = strWpStartTime.Substring(strWpStartTime.IndexOf("value=""") + "value=""".Length)
        strWpStartTime = strWpStartTime.Substring(0, strWpStartTime.IndexOf(""""))
        ' extract the edit time
        strWpEditTime = strResponseBody
        strWpEditTime = strWpEditTime.Substring(strWpEditTime.IndexOf("<form id=""editform"" "))
        strWpEditTime = strWpEditTime.Substring(strWpEditTime.IndexOf("<input") + "<input".Length)
        strWpEditTime = strWpEditTime.Substring(strWpEditTime.IndexOf("<input") + "<input".Length)
        strWpEditTime = strWpEditTime.Substring(strWpEditTime.IndexOf("<input") + "<input".Length)
        strWpEditTime = strWpEditTime.Substring(strWpEditTime.IndexOf("<input"))
        strWpEditTime = strWpEditTime.Substring(0, strWpEditTime.IndexOf("/>") + "/>".Length)
        If (strWpEditTime.IndexOf("name=""wpEdittime""") = -1) Then
            Throw New System.InvalidOperationException("failed to get edit time")
        End If
        strWpEditTime = strWpEditTime.Substring(strWpEditTime.IndexOf("value=""") + "value=""".Length)
        strWpEditTime = strWpEditTime.Substring(0, strWpEditTime.IndexOf(""""))
        ' initialise some variables used to build the post data
        strBoundary = "---------------------------" & DateTime.Now.Ticks.ToString("x")
        objPostData = New System.Text.StringBuilder
        ' wpLogin
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpLogin""")
        objPostData.AppendLine()
        objPostData.AppendLine()
        ' wpSection
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpSection""")
        objPostData.AppendLine()
        objPostData.AppendLine()
        ' wpStartTime
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpStarttime""")
        objPostData.AppendLine()
        objPostData.AppendLine(strWpStartTime)
        ' wpEditTime
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpEdittime""")
        objPostData.AppendLine()
        objPostData.AppendLine(strWpEditTime)
        ' wpScrolltop
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpScrolltop""")
        objPostData.AppendLine()
        objPostData.AppendLine(0.ToString)
        ' wpTextbox1
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpTextbox1""")
        objPostData.AppendLine()
        objPostData.AppendLine(body)
        ' wpSummary
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpSummary""")
        objPostData.AppendLine()
        objPostData.AppendLine(summary)
        ' wpMinoredit
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpMinoredit""")
        objPostData.AppendLine()
        If minor Then
            objPostData.AppendLine(1.ToString)
        Else
            objPostData.AppendLine(0.ToString)
        End If
        ' wpWatchthis
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpWatchthis""")
        objPostData.AppendLine()
        If watchThis Then
            objPostData.AppendLine(1.ToString)
        Else
            objPostData.AppendLine(0.ToString)
        End If
        ' wpSave
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpSave""")
        objPostData.AppendLine()
        objPostData.AppendLine("Save page")
        ' wpEditToken
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpEditToken""")
        objPostData.AppendLine()
        objPostData.AppendLine(strWpEditToken)
        ' wpAutoSummary
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpAutoSummary""")
        objPostData.AppendLine()
        objPostData.AppendLine(strWpAutoSummary)
        ' wpSection
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""antispam""")
        objPostData.AppendLine()
        objPostData.AppendLine()
        ' closing boundary
        objPostData.AppendLine("--" & strBoundary & "--")
        objPostData.AppendLine()
        ' build the uri to query
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        If Not (Me.IndexPath = String.Empty) Then
            objRequestUri.Append("/" & Me.IndexPath)
        End If
        objRequestUri.Append("/index.php?")
        objRequestUri.Append("title=" & System.Web.HttpUtility.UrlEncode(title))
        objRequestUri.Append("&action=submit")
        ' build the request
        objRequest = Me.CreateRequest(cookies, objRequestUri.ToString)
        objRequest.Method = "POST"
        objRequest.ContentType = "multipart/form-data; boundary=" & strBoundary
        ' set the content length
        objPostDataBytes = System.Text.Encoding.UTF8.GetBytes(objPostData.ToString())
        objRequest.ContentLength = objPostDataBytes.Length
        ' query the wiki server
        objResponse = Me.ExecuteRequest(objRequest, objPostDataBytes)
        If Not (objResponse.StatusCode = Net.HttpStatusCode.MovedPermanently) Then
            Throw New System.InvalidOperationException("save failed")
        End If
    End Sub

#End Region

#Region " Special Page Methods "

    ''' <summary>
    ''' Gets the list of pages in the WhatLinksHere special page for the specified article.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <param name="title">The title of the article to get the history for. This should be
    ''' non-urlencoded as it will be UrlEncoded inside the member.</param>
    ''' <param name="limit">The maximum number of pages to return in the collection. If the specified article
    ''' is used in more pages than the limit only the first articles are returned.</param>
    ''' <returns>Returns a collection of pages that link to the specified article</returns>
    ''' <remarks></remarks>
    Friend Function GetSpecialWhatLinksHere(ByVal cookies As System.Net.CookieContainer, ByVal title As String, ByVal limit As Integer) As WikiPageCollection
        Dim objRequestUri As System.Text.StringBuilder
        Dim strResponseBody As String
        Dim objResponseXml As System.Xml.XmlDocument
        Dim objResponseNodes As System.Xml.XmlNodeList
        Dim objWikiPages As WikiPageCollection
        Dim objWikiPage As WikiPage
        ' build the uri to query
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        If Not (Me.IndexPath = String.Empty) Then
            objRequestUri.Append("/" & Me.IndexPath)
        End If
        objRequestUri.Append("/index.php?")
        objRequestUri.Append("title=Special:Whatlinkshere/" & System.Web.HttpUtility.UrlEncode(title))
        objRequestUri.Append("&limit=" & limit)
        objRequestUri.Append("&from=0")
        ' query the url
        strResponseBody = Me.ExecuteUrl(cookies, objRequestUri.ToString)
        ' extract the whatlinkshere articles
        strResponseBody = strResponseBody.Substring(strResponseBody.IndexOf("<!-- start content -->"))
        strResponseBody = strResponseBody.Substring(strResponseBody.IndexOf("<ul>"))
        strResponseBody = strResponseBody.Substring(0, strResponseBody.IndexOf("</ul>") + "</ul>".Length)
        ' read the wiki response into an xml document
        objResponseXml = New System.Xml.XmlDocument
        objResponseXml.LoadXml(strResponseBody)
        ' convert article nodes into Wikipage objects
        objResponseNodes = objResponseXml.DocumentElement.SelectNodes("li")
        objWikiPages = New WikiPageCollection
        For Each objResponseNode As System.Xml.XmlNode In objResponseNodes
            objWikiPage = New WikiPage(Me, objResponseNode.SelectSingleNode("a").Attributes.GetNamedItem("title").Value)
            objWikiPages.Add(objWikiPage)
        Next objResponseNode
        ' return the result
        Return objWikiPages
    End Function

    ''' <summary>
    ''' Gets the list of pages in the UncategorizedImages special page.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <param name="limit">The maximum number of pages to return in the collection. If the specified article
    ''' is used in more pages than the limit only the first articles are returned.</param>
    ''' <returns>Returns a collection of pages that represent uncategorised images.</returns>
    ''' <remarks></remarks>
    Friend Function GetSpecialUncategorizedImages(ByVal cookies As System.Net.CookieContainer, ByVal limit As Integer) As WikiPageCollection
        Dim objRequestUri As System.Text.StringBuilder
        Dim strResponseBody As String
        Dim objResponseXml As System.Xml.XmlDocument
        Dim objResponseNodes As System.Xml.XmlNodeList
        Dim objWikiPages As WikiPageCollection
        Dim objWikiPage As WikiPage
        Dim strTitle As String
        ' build the uri to query
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        If Not (Me.IndexPath = String.Empty) Then
            objRequestUri.Append("/" & Me.IndexPath)
        End If
        objRequestUri.Append("/index.php?")
        objRequestUri.Append("title=Special:Uncategorizedimages")
        objRequestUri.Append("&limit=" & limit)
        objRequestUri.Append("&from=0")
        ' query the url
        strResponseBody = Me.ExecuteUrl(cookies, objRequestUri.ToString)
        ' extract the uncategorizedimages entries
        strResponseBody = strResponseBody.Substring(strResponseBody.IndexOf("<!-- start content -->"))
        strResponseBody = strResponseBody.Substring(strResponseBody.IndexOf("<table class=""gallery"" cellspacing=""0"" cellpadding=""0"">"))
        strResponseBody = strResponseBody.Substring(0, strResponseBody.IndexOf("</table>") + "</table>".Length)
        ' read the wiki response into an xml document
        objResponseXml = New System.Xml.XmlDocument
        objResponseXml.LoadXml(strResponseBody)
        ' convert image nodes into WikiPage objects
        objResponseNodes = objResponseXml.DocumentElement.SelectNodes("//div[@class = 'thumb']/div/a")
        objWikiPages = New WikiPageCollection
        For Each objResponseNode As System.Xml.XmlNode In objResponseNodes
            strTitle = objResponseNode.Attributes.GetNamedItem("href").Value
            strTitle = strTitle.Substring(strTitle.LastIndexOf("/") + 1)
            strTitle = System.Web.HttpUtility.UrlDecode(strTitle)
            objWikiPage = New WikiPage(Me, strTitle)
            objWikiPages.Add(objWikiPage)
        Next objResponseNode
        ' return the result
        Return objWikiPages
    End Function

#End Region

#Region " Image Methods "

    ''' <summary>
    ''' Uploads an image.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <param name="targetFileName">The target file name to upload to on the Wikia server.</param>
    ''' <param name="description">The description of the image as stored by the Wikia server.</param>
    ''' <param name="license">License information for the image.</param>
    ''' <param name="watchThis">Specifies whether the page should be marked as a watched page.</param>
    ''' <param name="fileBytes">The binary array of bytes that represent the image file.</param>
    ''' <param name="contentType">The MIME content type of the image.</param>
    ''' <remarks></remarks>
    Friend Sub UploadImage(ByVal cookies As System.Net.CookieContainer, ByVal targetFileName As String, ByVal description As String, ByVal license As String, ByVal watchThis As Boolean, ByVal fileBytes() As Byte, ByVal contentType As String)
        Dim objRequestUri As System.Text.StringBuilder
        Dim objRequest As System.Net.HttpWebRequest
        Dim objRequestStream As System.IO.Stream
        Dim objPostData As System.Text.StringBuilder
        Dim objPostDataBytes1() As Byte
        Dim objPostDataBytes2() As Byte
        Dim strBoundary As String
        Dim objBoundaryBytes() As Byte
        Dim objResponse As System.Net.HttpWebResponse
        Dim strResponseBody As String
        Dim strFormValue As String
        ' validate the parameters
        If (cookies Is Nothing) Then Throw New System.ArgumentNullException("cookies")
        If (targetFileName = String.Empty) Then Throw New System.ArgumentNullException("targetFileName")
        If (fileBytes Is Nothing) Then Throw New System.ArgumentNullException("fileBytes")
        If (System.IO.Path.GetFileNameWithoutExtension(targetFileName).Length < 3) Then
            Throw New System.ArgumentException("Target filename must contain at least 3 characters, excluding extension.")
        End If
        ' build the uri to query
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        If Not (Me.IndexPath = String.Empty) Then
            objRequestUri.Append("/" & Me.IndexPath)
        End If
        objRequestUri.Append("/index.php?")
        objRequestUri.Append("title=Special:Upload")
        objRequestUri.Append("&action=submit")
        ' initialise some variables used to build the post data
        strBoundary = "---------------------------" & DateTime.Now.Ticks.ToString("x")
        objPostData = New System.Text.StringBuilder
        ' wpUploadFile
        objPostData.AppendLine("--" & strBoundary)
        objPostData.Append("Content-Disposition: form-data; name=""wpUploadFile""; filename=""c:\temp\image.png""")
        objPostData.AppendLine("Content-Type: " & contentType)
        objPostData.AppendLine()
        objPostDataBytes1 = System.Text.Encoding.UTF8.GetBytes(objPostData.ToString())
        ' wpSourceType
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpSourceType""")
        objPostData.AppendLine()
        objPostData.AppendLine("file")
        ' wpDestFile
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpDestFile""")
        objPostData.AppendLine()
        objPostData.AppendLine(targetFileName)
        ' wpUploadDescription
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpUploadDescription""")
        objPostData.AppendLine()
        objPostData.AppendLine(description)
        ' wpLicense
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpLicense""")
        objPostData.AppendLine()
        objPostData.AppendLine(license)
        ' wpWatchthis
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpWatchthis""")
        objPostData.AppendLine()
        objPostData.AppendLine(watchThis.ToString.ToLower)
        ' wpUpload
        objPostData.AppendLine("--" & strBoundary)
        objPostData.AppendLine("Content-Disposition: form-data; name=""wpUpload""")
        objPostData.AppendLine()
        objPostData.AppendLine("Upload file")
        ' build the request
        objRequest = Me.CreateRequest(cookies, objRequestUri.ToString)
        objRequest.Method = "POST"
        objRequest.ContentType = "multipart/form-data; boundary=" & strBoundary
        ' set the content length
        objPostDataBytes2 = System.Text.Encoding.UTF8.GetBytes(objPostData.ToString())
        objBoundaryBytes = System.Text.Encoding.ASCII.GetBytes(ControlChars.CrLf & "--" & strBoundary & ControlChars.CrLf)
        objRequest.ContentLength = objPostDataBytes1.Length + fileBytes.Length + objPostDataBytes2.Length + objBoundaryBytes.Length
        ' write the request bytes
        objRequestStream = objRequest.GetRequestStream()
        objRequestStream.Write(objPostDataBytes1, 0, objPostDataBytes1.Length)
        objRequestStream.Write(fileBytes, 0, fileBytes.Length)
        objRequestStream.Write(objPostDataBytes2, 0, objPostDataBytes2.Length)
        objRequestStream.Write(objBoundaryBytes, 0, objBoundaryBytes.Length)
        objRequestStream.Close()
        ' query the wiki server
        objResponse = Me.ExecuteRequest(objRequest)
        strResponseBody = Me.GetResponseStreamText(objResponse)
        objResponse.Close()
        ' check the result
        If (strResponseBody.IndexOf("<span class='error'>") > -1) Then
            Throw New System.InvalidOperationException("upload failed")
        ElseIf (strResponseBody.IndexOf("File name has been changed to") > -1) Then
            Throw New System.InvalidOperationException("invalid filename")
        ElseIf (strResponseBody.IndexOf("<div class=""fullImageLink"" id=""file"">") > -1) Then
            ' the image didn't already exist and we managed to upload ok - this is no longer used since
            ' mediawiki now returns a 301 status code on uploads
            Debug.WriteLine("image uploaded")
        ElseIf (objResponse.StatusCode = Net.HttpStatusCode.Moved) Then
            ' the image didn't already exist and we possibly managed to upload ok, but check the location header first
            If (objResponse.Headers("Location") = Nothing) Then
                Throw New System.InvalidOperationException("no location header received while attemping go upload file")
            ElseIf Not System.Web.HttpUtility.UrlDecode(objResponse.Headers("Location")).EndsWith("/Image:" & targetFileName) Then
                Throw New System.InvalidOperationException("unexpected location header value received while attemping go upload file")
            Else
                Debug.Write("image uploaded")
            End If
        ElseIf (strResponseBody.IndexOf("<h2>Upload warning</h2>") = -1) Then
            Throw New System.InvalidOperationException("upload failed")
        Else
            ' the file already exists so we'll get a response that asks us to confirm the overwrite
            strBoundary = "---------------------------" & DateTime.Now.Ticks.ToString("x")
            objRequest = Me.CreateRequest(cookies, objRequestUri.ToString)
            objRequest.Method = "POST"
            objRequest.ContentType = "multipart/form-data; boundary=" & strBoundary
            ' wpIgnoreWarning
            objPostData = New System.Text.StringBuilder
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpIgnoreWarning""")
            objPostData.AppendLine()
            objPostData.AppendLine(1.ToString)
            ' wpSessionKey
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpSessionKey""")
            objPostData.AppendLine()
            strFormValue = strResponseBody
            strFormValue = strFormValue.Substring(strFormValue.IndexOf("<input type='hidden' name='wpSessionKey'"))
            strFormValue = strFormValue.Substring(strFormValue.IndexOf("value="""))
            strFormValue = strFormValue.Substring("value=""".Length)
            strFormValue = strFormValue.Substring(0, strFormValue.IndexOf(""""))
            objPostData.AppendLine(strFormValue & ControlChars.CrLf)
            ' wpUploadDescription
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpUploadDescription""")
            objPostData.AppendLine()
            objPostData.AppendLine(description)
            ' wpLicense
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpLicense""")
            objPostData.AppendLine()
            objPostData.AppendLine(license)
            ' wpDestFile
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpDestFile""")
            objPostData.AppendLine()
            objPostData.AppendLine(targetFileName)
            ' wpWatchthis
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpWatchthis""")
            objPostData.AppendLine()
            objPostData.AppendLine(watchThis.ToString.ToLower)
            ' wpUpload
            objPostData.AppendLine("--" & strBoundary)
            objPostData.AppendLine("Content-Disposition: form-data; name=""wpUpload""")
            objPostData.AppendLine()
            objPostData.AppendLine("Save file")
            ' set the content length
            objPostDataBytes1 = System.Text.Encoding.UTF8.GetBytes(objPostData.ToString())
            objBoundaryBytes = System.Text.Encoding.ASCII.GetBytes(ControlChars.CrLf & "--" & strBoundary & ControlChars.CrLf)
            objRequest.ContentLength = objPostDataBytes1.Length + objBoundaryBytes.Length
            ' write the request bytes
            objRequestStream = objRequest.GetRequestStream()
            objRequestStream.Write(objPostDataBytes1, 0, objPostDataBytes1.Length)
            objRequestStream.Write(objBoundaryBytes, 0, objBoundaryBytes.Length)
            objRequestStream.Close()
            ' query the wiki server
            objResponse = Me.ExecuteRequest(objRequest)
            strResponseBody = Me.GetResponseStreamText(objResponse)
            objResponse.Close()
            ' validate the response
            If (objResponse.StatusCode = Net.HttpStatusCode.Moved) Then
                If (objResponse.Headers("Location") = Nothing) Then
                    Throw New System.InvalidOperationException("no location header received while attemping go upload file")
                ElseIf Not System.Web.HttpUtility.UrlDecode(objResponse.Headers("Location")).EndsWith("/Image:" & targetFileName) Then
                    Throw New System.InvalidOperationException("unexpected location header value received while attemping go upload file")
                Else
                    Debug.WriteLine("image uploaded")
                End If
            Else
                Throw New System.InvalidOperationException("an error occurred while confirming image upload")
            End If
        End If
    End Sub

    ''' <summary>
    ''' Returns the result of calling the QueryImageInfo api function.
    ''' </summary>
    ''' <param name="cookies">The authentication cookie to use send when making any http requests
    ''' inside the method.</param>
    ''' <param name="title">The image filename to get information for.</param>
    ''' <returns>A WikiPageCollection that contains information about each revision of the image.</returns>
    ''' <remarks></remarks>
    Friend Function ApiQueryImageInfo(ByVal cookies As System.Net.CookieContainer, ByVal title As String) As WikiPageCollection
        Dim objRequestUri As System.Text.StringBuilder
        Dim strResponseBody As String
        Dim objResponseXml As System.Xml.XmlDocument
        Dim objResponseNodes As System.Xml.XmlNodeList
        Dim objWikiPages As WikiPageCollection
        Dim objWikiPage As WikiPage
        ' build the uri to query
        objRequestUri = New System.Text.StringBuilder
        objRequestUri.Append(Me.SiteUrl)
        objRequestUri.Append("/api.php?")
        objRequestUri.Append("action=query")
        objRequestUri.Append("&titles=" & title)
        objRequestUri.Append("&prop=imageinfo")
        objRequestUri.Append("&format=xml")
        ' query the wiki server
        strResponseBody = Me.ExecuteUrl(cookies, objRequestUri.ToString)
        ' read the wiki response into an xml document
        objResponseXml = New System.Xml.XmlDocument
        objResponseXml.LoadXml(strResponseBody)
        ' convert page nodes into WikiPage objects
        objResponseNodes = objResponseXml.DocumentElement.SelectNodes("/api/query/pages/page")
        objWikiPages = New WikiPageCollection
        For Each objResponseNode As System.Xml.XmlNode In objResponseNodes
            If (objResponseNode.SelectNodes("imageinfo").Count > 0) Then
                objWikiPage = New WikiPage(Me, title)
                objWikiPage.RevisionId = CType(objResponseNode.Attributes("pageid").Value, Integer)
                objWikiPage.EditDate = Date.ParseExact(objResponseNode.SelectSingleNode("imageinfo/ii").Attributes("timestamp").Value, "yyyy-MM-dd'T'HH:mm:ss'Z'", System.Globalization.CultureInfo.InvariantCulture)
                objWikiPage.EditUser = objResponseNode.SelectSingleNode("imageinfo/ii").Attributes("user").Value
                objWikiPage.EditMinor = False
                objWikiPages.Add(objWikiPage)
            End If
        Next objResponseNode
        ' return the result
        Return objWikiPages
    End Function

#End Region

#End Region

End Class

#End Region
